package NeuronNetworkLibrary;

/**
 * Sample neuron implementation.
 *
 * @author Zbyszko
 */
public final class Neuron {

    private double[] inputWeights;
    private double[] inputValues;
    private double[] previousWeights; // necessary to calculate the momentum.
    private double calculatedOutput;
    private double desiredOutput;
    private double delta;
    private String activationFuncion; // indicates which activation functions is used. 

    /**
     * Sets the values Neurons on input layer. (Used only for visualization of
     * input layer).
     *
     * @param patternInputValues Neuron's initial values inputed in input layer.
     */
    public void setInputNeuron(double patternInputValues) {

        this.calculatedOutput = patternInputValues;// *1
    }

    /**
     * Sets and calculates the values for hidden layer.
     *
     * @param inputValues Neuron's input values.
     * @param activationFunc Determines the activation function which will be
     * used.
     */
    public void setHiddenNeuron(double[] inputValues, String activationFunc) {

        this.inputValues = inputValues;
        this.activationFuncion = activationFunc;
        if (Network.INIT_WEIGHTS_FLAG) {
            inputWeights = new double[inputValues.length];
            this.initWeights();
        }

        this.calculateWeightedSum_Hidden();

    }

    /**
     * Sets and calculates the values for output layer.
     *
     * @param inputValues Neuron's input values.
     * @param desiredOutput Neuron's desired output values.
     * @param activationFunc Determines the activation function which will be
     * used.
     */
    public void setOutputNeuron(double[] inputValues, double desiredOutput, String activationFunc) {

        this.inputValues = inputValues;
        this.activationFuncion = activationFunc;
        this.desiredOutput = desiredOutput;
        if (Network.INIT_WEIGHTS_FLAG) {
            inputWeights = new double[inputValues.length];
            this.initWeights();
        }
        this.calculateWeightedSum_Output();
    }

    /**
     * Randomly inits the weights.
     */
    private void initWeights() {
        for (int j = 0; j < inputWeights.length; j++) {
            inputWeights[j] = Math.random() - Network.MAX_WEIGHT;  //[-0.5,0.5)
        }
    }

    /**
     * Calculates the weighted sum for hidden layer.
     */
    public void calculateWeightedSum_Hidden() {

        this.calculatedOutput = 0; //it needs to be zero
        for (int i = 0; i < inputValues.length; i++) {
            calculatedOutput += inputValues[i] * inputWeights[i];
        }

        switch (this.activationFuncion) {
            case "linear":
                calculatedOutput = ActivationFunction.linearFunction(calculatedOutput);
                break;
            case "sigmo":
                calculatedOutput = ActivationFunction.sigmoidalFunction(calculatedOutput);
                break;
            default:
                calculatedOutput = ActivationFunction.sinusoidalFunction(calculatedOutput);
                break;
        }
    }

    /**
     * Calculates the weighted sum for output layer.
     */
    public void calculateWeightedSum_Output() {

        this.calculatedOutput = 0;
        for (int i = 0; i < inputValues.length; i++) {
            calculatedOutput += inputValues[i] * inputWeights[i];
        }
        /**
         * PL: Liczenie delty: Pochodna funkcji f=x wynosi f'=1, dlatego
         * teoretycznie powinniśmy mnożyć jeszcze razy 1 Pochodna funkcji
         * f=1/(1+e^-ßx) wynosi f'=ß*(1-f)*f Pochodna funkcji f=sin(x) wynosi
         * f'=cos(x), dlatego w tym przypadku mamy mnożenie razy cos(x).
         * ---------------------------------------------------------------------
         * ENG: Delta calculation: The derivative of function f=x is f'=1, so
         * theoretically we should multiply it by 1. The derivate of function
         * f=1/(1+e^-ßx) is f'=ß*(1-f)*f The derivate of f=sin(x) is f'=cos(x),
         * so in that case, we multiply by cos(x).
         */
        switch (activationFuncion) {
            case "linear":
                calculatedOutput = ActivationFunction.linearFunction(calculatedOutput);
                delta = desiredOutput - calculatedOutput;
                break;
            case "sigmo":
                calculatedOutput = ActivationFunction.sigmoidalFunction(calculatedOutput);
                delta = 0.5 * calculatedOutput * (1 - calculatedOutput) * (desiredOutput - calculatedOutput);
                break;
            default:
                calculatedOutput = ActivationFunction.sinusoidalFunction(calculatedOutput);
                delta = (desiredOutput - calculatedOutput) * Math.cos(calculatedOutput);
                break;
        }
    }

    /**
     * Returns the calculated output for the neuron.
     *
     * @return
     */
    public double getCalculatedOutput() {
        return calculatedOutput;
    }

    /**
     * Returns the delta value.
     *
     * @return
     */
    public double getDelta() {
        return delta;
    }

    /**
     * Sets the delta values.
     *
     * @param weights
     * @param deltas
     */
    public void setDelta(double[] weights, double[] deltas) {
        double deltaSum = 0;
        for (int i = 0; i < deltas.length; i++) {
            deltaSum += weights[i] * deltas[i];
        }

        switch (activationFuncion) {
            case "linear":
                this.delta = deltaSum;
                break;
            case "sigmo":
                this.delta = 0.5 * calculatedOutput * (1 - calculatedOutput) * deltaSum;
                break;
            default:
                this.delta = Math.cos(calculatedOutput) * deltaSum;
                break;
        }
    }

    /**
     * Returns the activation function name.
     *
     * @return
     */
    public String getActivationFuncion() {
        return activationFuncion;
    }

    /**
     * Sets the array of input weights.
     *
     * @param inputWeights Input weights.
     */
    public void setInputWeights(double[] inputWeights) {
        this.inputWeights = inputWeights;
    }

    /**
     * Returns the array with input weights.
     *
     * @return
     */
    public double[] getInputWeights() {
        return inputWeights;
    }

    /**
     * Returns the array of input values.
     *
     * @return
     */
    public double[] getInputValues() {
        return inputValues;
    }

    /**
     * Returns the array of previously assigned weightes.
     *
     * @return
     */
    public double[] getPreviousWeights() {
        return previousWeights;
    }

    /**
     * Sets the previous weights array.
     *
     * @param previousWeights Previous weights array.
     */
    public void setPreviousWeights(double[] previousWeights) {
        this.previousWeights = previousWeights;
    }

    /**
     * Returns the desired output.
     *
     * @return
     */
    public double getDesiredOutput() {
        return desiredOutput;
    }

    @Override
    public String toString() {
        return ""//"Weights: " + Arrays.toString(inputWeights) + "\n" 
                //+ "Input:" + Arrays.toString(inputValues) + "\n"
                + "Desired: " + desiredOutput + " "
                + "Output: " + calculatedOutput + " "
                + "Delta: " + delta + " ";
    }
}